function TextLayout(textModel){
    this.init$Eventable();
    
    this.$textModel = textModel;
    this.$runPartitions = [[0, [1, _RUN_EOF, null], 1, 0]];
    this.$cachedRun = null;
    this.$cachedRunOffset = 0;
    this.$tabSize = 4;
    this.$maxColumn = 0;
    this.$wrapEnabled = false;
    this.$wrapData = null;
    this.$rowCache = [];
    this.$rowCountCache = 0;
    this.$printMarginColumn = 120;

    if (this.$textModel.getLength()) {
        this._updateRuns(0, 0, this.$textModel.getText());
        
    }
    this.on(this.$textModel, 'Changing', function (evt){
        this._updateRuns(evt.offset, evt.offset + evt.deletedText.length, evt.insertedText);
    });
    this.on(this.$textModel, 'LinesChanged', this._onLinesChanged);
    this._initLineData();
}

var _RUN_ZERO = 0;
var _RUN_CHAR = 1;
var _RUN_CJK_CHAR = 2;
var _RUN_TAB = 4;
var _RUN_EOF = -1;

var _FLOOR = -1;
var _ROUND = 0;
var _CEIL = 1;

(function (){
    // fields of run.
    var $length = 0;
    var $kind = 1;
    var $next = 2;

    // fields of partiton
    var $offset = 0;
    var $run = 1;
    var $runCount = 2;
    var $index = 3;

    // fields of lineData["TextLayout.data"][rowIndex]
    var $rowOffset = 0;
    var $endColumn = 1;

    var runRegex = /([\t]+)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]+)|[\v\f\uFEFF\u200B\u200C\u200D]+/g;

    //todo 当启用wrap模式时，maxColumn已经确定，可以只去layout显示部分的那些行. 当然上下的部分行也可以延迟layout，这样才能让垂直滚动条看起来正常。
    //todo 尝试将wrap部分独立出来作为WrappedTextLayout

    TextLayout.prototype = {
//+==================
//+ Run处理部分.
//+==================
        $splitRunCount: 256,
        // 调试用
        _dbg_print: function (){
            var run = this.$runPartitions[0][$run];
            var buff = [];
            while (run != null) {
                buff.push(new Array(run[$length] + 1).join(run[$kind]));
                run = run[$next];
            }
            console.log(buff.join(''));
        },
        _updateRuns: function (startOffset, endOffset, text){
            // todo: 在partition上记录column, 并增加_RUN_BREAK. （这样可以解决一行上问题.）
            var r1 = this._findRun(startOffset);
            var startRun = r1[0];
            var startRunOffset = r1[1];
            var runIndex = r1[2];
            var startPartition = r1[3] || this._findPartition(startOffset);
            var r2 = this._findRun(endOffset, startRun, startRunOffset);
            var endRun = r2[0];
            var endRunOffset = r2[1];
            var endRunIndex = runIndex + r2[2];
            var countDelta = - r2[2]; //r2[2]是为删除掉的个数.
            var lengthDelta = text.length - (endOffset - startOffset);

            // 处理删除区间两端的run.
            if (startRun === endRun) {
                endRun = startRun.slice(0);
                countDelta ++;
            }
            startRun[$next] = endRun;
            
            startRun[$length] = startOffset - startRunOffset;
            endRun[$length] = endRunOffset + endRun[$length] - endOffset;
            
            // 创建插入文本的runs并链接他们.
            if (text.length) {
                var r3 = this._getTextRuns(text);
                var headRun = r3[0];
                var tailRun = r3[1];
                startRun[$next] = headRun;
                tailRun[$next] = endRun;
                countDelta += r3[2];
                if (tailRun[$kind] === endRun[$kind]) {
                    tailRun[$length] += endRun[$length];
                    tailRun[$next] = endRun[$next];
                    countDelta --;
                    // 更新endRun的引用, 使其始终指向最后一个run.
                    endRun = tailRun;
                }
                if (startRun[$kind] === headRun[$kind]) {
                    startRun[$length] += headRun[$length];
                    startRun[$next] = headRun[$next];
                    countDelta --;
                    // 更新endRun的引用, 使其始终指向最后一个run.
                    if (headRun === endRun) {
                        endRun = startRun;
                    }
                }
            }
            if (startRun[$length] == 0) {
                var nextRun = startRun[$next];
                startRun[$length] = nextRun[$length];
                startRun[$kind] = nextRun[$kind];
                startRun[$next] = nextRun[$next];
                if (nextRun === endRun) {
                    endRun = startRun;
                }
            }

            // 更新partitions.
            var partitions = this.$runPartitions;
            var startPartitionIndex = startPartition[$index];
            var endPartitionIndex = startPartitionIndex;
            var partitionEndRunIndex = 0;
            for (var i=startPartitionIndex; i<partitions.length; i++) {
                var p = partitions[i];
                if (endRunIndex < partitionEndRunIndex + p[$runCount]) {
                    endPartitionIndex = i;
                    break;
                }
                partitionEndRunIndex += p[$runCount];
            }
            
            var insertPartitionIndex = startPartitionIndex + 1;
            var toBeDeletedPartitionCount = endPartitionIndex - startPartitionIndex;
            var toInsertPartitionCount = 0;
            var splitRunCount = this.$splitRunCount;
            var tidyRunCount = splitRunCount >> 1;
            
            var endPartition = partitions[endPartitionIndex];
            if (endPartition !== startPartition) {
                endPartition[$run] = endRun;
                // 因这个endPartition及之后的partitions都需要校正其offset, 所以放到后面一起处理.
                endPartition[$runCount] += countDelta;
                if (endPartition[$runCount] < tidyRunCount) {
                    // 这个时候startPartition和endPartition其实已经是连着的了. 所以可以这么操作.
                    toBeDeletedPartitionCount ++;
                    startPartition[$runCount] += endPartition[$runCount];
                }
            }

            var startPartitionRunCount = (startPartition[$runCount] += countDelta);
            var array = [insertPartitionIndex, toBeDeletedPartitionCount];
            if (startPartition[$runCount] > splitRunCount) {
                toInsertPartitionCount = Math.floor(startPartition[$runCount] / tidyRunCount) - 1;
                Utils.assert(toInsertPartitionCount >= 1, '');
                startPartition[$runCount] = tidyRunCount + (startPartitionRunCount % tidyRunCount);
                var lastPartition = startPartition;
                for (var j=0; j<toInsertPartitionCount; j++) {
                    array.push(lastPartition = this._createNextPartition(lastPartition, tidyRunCount));
                }
            }
            
            if (toBeDeletedPartitionCount > 0 || toInsertPartitionCount > 0) {
                partitions.splice.apply(partitions, array);
            }
            for (var k=insertPartitionIndex+toInsertPartitionCount; k<partitions.length; k++) {
                var partition = partitions[k];
                partition[$index] = k;
                partition[$offset] += lengthDelta;
            }
            if (this.$cachedRun != null && endOffset <= this.$cachedRunOffset) {
                this.$cachedRun = null;
            }
            // 所有更新结束. ho~~~
        },
        _createNextPartition: function (prevPartition, runCount){
            var run = prevPartition[$run];
            var offset = prevPartition[$offset];
            var index = prevPartition[$index];
            var count = prevPartition[$runCount];
            for (var i=0; i<count; i++) {
                offset += run[$length];
                run = run[$next];
            }
            return [offset, run, runCount, index + 1];
        },
        _getTextRuns: function (text){
            var rootRun = [];
            var lastRun = rootRun;
            var lastIndex = 0;
            var count = 0;
            var match;
            runRegex.lastIndex = 0;
            while (match = runRegex.exec(text)) {
                var index = match.index;
                if (index > lastIndex) {
                    lastRun[$next] = [index - lastIndex, _RUN_CHAR];
                    lastRun = lastRun[$next];
                    count ++;
                }
                lastIndex = runRegex.lastIndex;
                lastRun[$next] = [match[0].length, match[1] ? _RUN_TAB : match[2] ? _RUN_CJK_CHAR : _RUN_CHAR];
                lastRun = lastRun[$next];
                count ++;
            }
            if (lastIndex !== text.length) {
                lastRun[$next] = [text.length - lastIndex, _RUN_CHAR];
                lastRun = lastRun[$next];
                count ++;
            }
            return [rootRun[$next], lastRun, count];
        },
        _findPartition: function (offset){
            var partitions = this.$runPartitions;
            var left = 0;
            var right = partitions.length;
            while (left < right) {
                var middle = (left + right) >> 1;
                var middlePartition = partitions[middle];
                if (offset < middlePartition[$offset]) {
                    right = middle;
                } else {
                    var nextPartition = partitions[middle + 1];
                    if (nextPartition == null || offset < nextPartition[$offset]) {
                        left = right = middle;
                    } else if (offset === nextPartition[$offset]) {
                        left = right = middle + 1;
                    } else {
                        left = middle + 1;
                    }
                }
            }
            return partitions[left];
        },
        // 注意当offset大于某个run的右边界时，一定是返回其右边的run.
        _findRun: function (offset, run, runOffset){
            var partitions = this.$runPartitions;
            var partition = null;
            if (offset == 0) {
                return [partitions[0][$run], 0, 0, partitions[0]];
            }
            if (run == null) {
                if (this.$cachedRun != null) {
                    run = this.$cachedRun;
                    runOffset = this.$cachedRunOffset;
                    if (offset < runOffset || (offset - runOffset) > Math.max(4096, runOffset[$length])) {
                        this.$cachedRun = null;
                        run = null;
                    }
                }
                if (run == null) {
                    partition = this._findPartition(offset);
                    run = partition[$run];
                    runOffset = partition[$offset];
                }

            }
            var steps = 0;
            while (run != null) {
                if (runOffset + run[$length] > offset) {
                    break;
                }
                runOffset += run[$length];
                run = run[$next];
                steps ++;
            }
            this.$cachedRun = run;
            this.$cachedRunOffset = runOffset;
            return [run, runOffset, steps, partition];
        },
        _getRuns: function (startOffset, endOffset){
            var r1 = this._findRun(startOffset);
            var run = r1[0];
            var runOffset = r1[1];
            var runs = [];
            if (runOffset < startOffset) {
                run = [run[$length] - (startOffset-runOffset), run[$kind], run[$next]];
                runOffset = startOffset;
            }
            while (run != null) {
                if (run[$length] + runOffset >= endOffset) {
                    break;
                }
                runs.push(run.slice(0));
                runOffset += run[$length];
                run = run[$next];
            }

            // todo: updateRuns可能还有一些bug,所以这里暂时加上!=null判断.
            if (run != null && endOffset > runOffset) {
                runs.push([endOffset - runOffset, run[$kind], null]);
            }
            return runs;
        },
//+==================
//+ 坐标换算部分.
//+==================
        _onLinesChanged: function (evt){
            var lineIndex = evt.startIndex;
            // 其实第一行是有offsetInline可用的
            // 这样在计算wrap时可以不用管offsetInline所在row - 1之前的row了.
            // 专门用来对付合并成为一行的文本的wrap.
            var endIndex = evt.endIndex;
            if (this.$wrapEnabled) {
                this._invalidateRowCache(lineIndex);
            }
            for (var i=lineIndex; i<=endIndex; i++) {
                this._updateLineData(i);
            }
            this._deferUpdateLayout();
        },
        _initLineData: function (){
            var lineCount = this.$textModel.getLineCount();
            for (var i=0; i<lineCount; i++) {
                this._updateLineData(i);
            }
            this._updateLayout();
        },
        _getRunColumn: function (run, column){
            column = column | 0;
            switch (run[1]) {
            case _RUN_CHAR: return run[0];
            case _RUN_CJK_CHAR: return run[0] * 2;
            case _RUN_TAB: return this.$tabSize - column % this.$tabSize + this.$tabSize * (run[0] - 1);
            default: return 0;
            }
        },
        _getRunsColumn: function (runs, column){
            column = column | 0;
            for (var i=0; i<runs.length; i++) {
                column += this._getRunColumn(runs[i], column);
            }
            return column;
        },
        _computeColumn: function (offset, startOffset){
            return this._getRunsColumn(this._getRuns(startOffset | 0, offset));
        },
        _updateLineData: function (lineIndex){
            var data = [];
            if (!this.$wrapEnabled) {
                var endColumn = this._computeColumn(this.getRowEndOffset(lineIndex), this.getRowStartOffset(lineIndex));
                data[0] = [0, endColumn];
            }
            this.$textModel.setLineData(lineIndex, 'layoutData', data);
        },
        _updateLayout: function (){
            var rowCount = this.getRowCount();
            var columns = [];
            for (var i=0; i<rowCount; i++) {
                columns.push(this.getRowEndColumn(i));
            }
            this.$maxColumn = columns.reduceRight(function (a, b){
                return a < b ? b : a;
            }, 0);
//            this.$maxColumn = columns.reduceRight(function (initial, value){
//                return Math.max(initial, value);
//            }, 0);
            this.dispatchEvent({ type: 'LayoutChanged' });
        },
        _deferUpdateLayout: function (){
            clearTimeout(this._updateLayoutTimer);
            var this_ = this;
//            this._updateLayoutTimer = setTimeout(function (){
                this_._updateLayout();
//            });
//            this.dispatchEvent({ type: 'LayoutChanged' });
        },
        _invalidateRowCache: function (lineIndex){
            var curLength = this.$rowCache.length;
            if (lineIndex < curLength) {
                var oldLength = this.$rowCache._oldLength;
                if (oldLength == null) {
                    this.$rowCache._oldLength = curLength;
                }
                this.$rowCache.length = lineIndex;
                if (lineIndex < oldLength >> 2) {
                    this.$rowCache = this.$rowCache.slice(0, lineIndex);
                }
            }
        },
        getRowCount: function (){
            var lineCount = this.$textModel.getLineCount();
            if (!this.$wrapEnabled) {
                return lineCount;
            }
            if (this.$rowCountCache < lineCount) {
                this.$rowCountCache = 0;
                for (var i=0; i<lineCount; i++) {
                    this.$rowCountCache += this.$wrapData[i].length;
                }
            }
            return this.$rowCountCache;
        },
        getRow: function (lineIndex){
            return lineIndex;
        },
        getRowLineIndex: function (row){
            if (!this.$wrapEnabled) {
                return [row, 0];
            }
        },
        getRowEndColumn: function (row){
            var r = this.getRowLineIndex(row);
            var lineIndex = r[0];
            var rowIndex = r[1];
//            if (this.$textModel.getLineData(lineIndex, 'dirty')) {
//
//            }
            var data = this.$textModel.getLineData(lineIndex, 'layoutData');
            return data[rowIndex][$endColumn];
        },
        getMaxColumn: function (){
            return this.$maxColumn;
        },
        getRowStartOffset: function (row){
            return this.$textModel.getLineStartOffset(row);
        },
        getRowEndOffset: function (row){
            return this.$textModel.getLineEndOffset(row);
        },
        getRowColumnOffset: function (row, column, mode){
            mode = mode | 0; // 默认为_ROUND.
            if (column == 0) {
                return [this.getRowStartOffset(row), 0];
            } else if (column >= this.getRowEndColumn(row)) {
                return [this.getRowEndOffset(row), this.getRowEndColumn(row)];
            }
            var rowStartOffset = this.getRowStartOffset(row);
            var r = this._findRun(rowStartOffset);
            var run = r[0];
            var runOffset = r[1];
            run = [run[$length] - (rowStartOffset - runOffset), run[$kind], run[$next]];
            var runColumn;
            var stepColumn = 0;
            var stepOffset = rowStartOffset;
            while (run != null) {
                runColumn = this._getRunColumn(run, stepColumn);
                if (stepColumn + runColumn > column) {
                    break;
                }
                stepOffset += run[$length];
                stepColumn += runColumn;
                run = run[$next];
            }

            var deltaOffset;
            if (run == null || run[$kind] === _RUN_ZERO) {
                deltaOffset = 0;
            } else if (run[$kind] === _RUN_TAB) {
                deltaOffset = Math.floor((column - stepColumn) / this.$tabSize);
                stepColumn += deltaOffset * this.$tabSize;
                deltaOffset += mode === _CEIL ? 1 :
                    mode === _FLOOR ? 0 : column >= (stepColumn + stepColumn + runColumn) / 2 ? 1 : 0;
            } else {
                var fn = mode === _CEIL ? Math.ceil :
                    mode === _FLOOR ? Math.floor : Math.round;
                var unitColumn = run[$kind] === _RUN_CJK_CHAR ? 2 : 1;
                deltaOffset = fn.call(Math, (column - stepColumn) / unitColumn);
                stepColumn += deltaOffset * unitColumn;
            }
            return [stepOffset + deltaOffset, stepColumn];
        },
        getRowAndColumn: function (offset, row){
            if (row == null) {
                var rowCount = this.getRowCount();
                var left = 0;
                var right = this.getRowCount();
                while (left < right) {
                    var middle = (left + right) >> 1;
                    var next = middle + 1;
                    var middleStartOffset = this.getRowStartOffset(middle);
                    if (offset < middleStartOffset) {
                        right = middle;
                    } else {
                        if (next >= rowCount) {
                            left = right = next;
                        } else {
                            var nextRowStartOffset = this.getRowStartOffset(next);
                            if (offset > nextRowStartOffset) {
                                left = next;
                            } else if (offset == nextRowStartOffset) {
                                left = right = next;
                            } else {
                                left = right = middle;
                            }
                        }
                    }
                }
                row = left;
                // 因为之前的判断offset == nextRowStartOffset可能在最后一行时会出问题. 所以这里修一下.
                if (row >= this.getRowCount()) {
                    row = this.getRowCount() - 1;
                }
            }
            var rowStartOffset = this.getRowStartOffset(row);
            var column = this._computeColumn(offset, rowStartOffset);
            return [row, column];
        },
//+==================
//+ wrap处理部分.
//+==================
        isWrapEnabled: function (){
            return this.$wrapEnabled;
        },
        setWrapEnabled: function (val){
            this.$wrapEnabled = val;
            // todo:
        },
        getPrintMarginColumn: function (){
            return this.$printMarginColumn;
        },
        setPrintMarginColumn: function (val){
            this.$printMarginColumn = val;
        },
        // 这个方法一定是在_updateRuns后调用.
        _updateWrap: function (offset, length, text){
            if (!this.$wrapEnabled) {
                return;
            }
            
        },
//+==================
//+ 其他.
//+==================
        // todo 这个东西放在这里好讨厌啊. 考虑暴露run结构 交给外部去render
        renderInlineHTML: function (buff, startOffset, endOffset, fontInfo, row, column){
            var runs = this._getRuns(startOffset, endOffset);
            var stepColumn = column == null ? this.getRowAndColumn(startOffset, row)[1] : column;
            var stepOffset = startOffset;
            for (var i=0; i<runs.length; i++) {
                var run = runs[i];
                var runColumn = this._getRunColumn(run, stepColumn);
                if (run[1] == _RUN_TAB) {
                    buff.push(Utils.stringRepeat('&#160;', runColumn));
                } else {
                    var text = this.$textModel.getText(stepOffset, stepOffset + run[0]);
                    if (run[1] == _RUN_CJK_CHAR) {
                        var letterSpacing = fontInfo.__letterSpacing || (
                            fontInfo.__letterSpacing = fontInfo.charWidth * 2 - fontInfo.cjkCharWidth);
                        if (text.length === 1) {
                            buff.push('<span style="' +
                                'margin-left:', (letterSpacing >> 1) + 1, 'px;',
                                'margin-right:', (letterSpacing >> 1) - 1, 'px;',
                                '">', text, '</span>');
                        } else {
                            buff.push('<span style="' +
                                'letter-spacing:', letterSpacing, 'px;',
                                'margin-left:', (letterSpacing >> 1) + 1, 'px;',
                                'margin-right:', -((letterSpacing >> 1) + 1), 'px',
                                '">', text, '</span>');
                        }
                    } else if (run[1] == _RUN_CHAR) {
                        buff.push(Utils.escapeHTML(text));
                    }
                }
                stepOffset += run[0];
                stepColumn += runColumn;
            }
            return stepColumn;
        }
    };

})();

Utils.extend(TextLayout, Eventable);
